package sender;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;

import common.Constants;
import common.Header;
import common.PacketType;
import common.RTT;
import common.Utilities;


public class SendFileMain {     
    private static DatagramSocket socket;	
    private static int seqNum = 0;
    private static long curTimeout = 50;//Constants.TIMEOUT;
    private static RTT roundTripTimes = new RTT();
    // use this variable to move sliding window up when ackNum + window size > seqNum
    private static int highestAckNum = 0;
    
	public static void main(String[] args) {	
	    // parse command line
		SendArguments sendArgs = new SendArguments(args);

	    try {
	    	// create socket
	    	socket = new DatagramSocket(Constants.SENDER_PORT);
			InetAddress address = InetAddress.getByName(sendArgs.getHost());
		    
			++seqNum;
			// first packet sent is START
			byte[] data = Utilities.serializeString(sendArgs.getFileName());			
			Header startHeader = new Header(seqNum, PacketType.PACKET_START, data.length);
			byte[] serializedStartPacket = createPacketBuffer(startHeader, data);	
			
			// compute and set check sum for the packet
			long checkSum = Utilities.computeCheckSum(serializedStartPacket);
			startHeader.setCheckSum(checkSum);
			serializedStartPacket = createPacketBuffer(startHeader, data);
			
			// construct the packet
			DatagramPacket startPacket = createDatagramPacket(serializedStartPacket, 
															  address, 
															  sendArgs.getReceiverPort());
			
			// send the start packet, receive the ack, resends if timeout.
			System.out.println("Initial Timeout Time = " + curTimeout + "ms");
			sendReceive(startPacket, "0 (0)");
			seqNum++;
			
		    // open the file for reading (BufferedInputStream)
			File file = new File(sendArgs.getFileName());
		    FileInputStream fis = null;
		    BufferedInputStream bis = null;
			fis = new FileInputStream(file);
			bis = new BufferedInputStream(fis);
			
			long fileLength = file.length();
			System.out.println("----------------------------START SEND FILE----------------------------");
			System.out.println("Total bytes in file to send (aka file size): " + fileLength + "bytes");
			
		    // read in file data	
			int avail = bis.available();
			long fileOffset = 0;

			// read file in 1400 bytes at a time to be sent
			while(avail > 0) { // while there's more of the file to read
				int amount = Constants.DATA_SIZE;
				if(avail < Constants.DATA_SIZE) {
					amount = avail;
				}
				
				byte[] serializedFileChunk = new byte[amount];
			    // construct packet header
				Header packetDataHeader = new Header(seqNum, PacketType.PACKET_DATA, amount);
				// always offset = 0 because read() has side effect of moving the position to read in the file up
				bis.read(serializedFileChunk, 0, amount);
				byte[] serializedDataPacket = createPacketBuffer(packetDataHeader, serializedFileChunk);
				
				checkSum = Utilities.computeCheckSum(serializedDataPacket);
				packetDataHeader.setCheckSum(checkSum);
				serializedDataPacket = createPacketBuffer(packetDataHeader, serializedFileChunk);	
				
				// construct the packet
				DatagramPacket packet = createDatagramPacket(serializedDataPacket,
															 address, 
															 sendArgs.getReceiverPort());				
				
				// send the packet, receive the ack, resends if timeout.
				sendReceive(packet, fileOffset + " (" + serializedFileChunk.length + ")");
				seqNum++;
				
				/*
				 * TODO: 
				 * // determined by advertised window and last ack received to move up 
				 * // sliding window
				 * while(canSend) {
				 * 		in here do the above sending code (including the start packet)
				 * }
				 * TODO: test more packets waiting to be sent/resent
				 * 
				 */
				
				fileOffset += serializedFileChunk.length;
				avail = bis.available();
			}	

			System.out.println("----------------------------END SEND FILE----------------------------");
			
			// last packet sent is FINISH
			//TODO: optimization - last PACKET_DATA sent, make it the PACKET_FINISH
			Header packetFinishHeader = new Header(seqNum, PacketType.PACKET_FINISH, 0);
		    // serialize packet header
			byte[] serializedFinishHeader = packetFinishHeader.serialize();
			
			checkSum = Utilities.computeCheckSum(serializedFinishHeader);
			packetFinishHeader.setCheckSum(checkSum);
			serializedFinishHeader = packetFinishHeader.serialize();
			
			DatagramPacket finishPacket = new DatagramPacket(serializedFinishHeader, 
															 serializedFinishHeader.length,
															 address,
															 sendArgs.getReceiverPort());
			
			// send finish packet, receive the ack, resends if timeout.
			sendReceive(finishPacket, "0 (0)");	
			seqNum++;
						
			// close all of the resources
			fis.close();
			bis.close();
			socket.close();
			
			System.out.println("[completed]");
	    }
	    catch (FileNotFoundException e) {
	    	System.out.println(e.getMessage());
	    	e.printStackTrace();
	    } 
	    catch (IOException e) {
	    	System.out.println(e.getMessage());
	    	e.printStackTrace();
	    }
	} // end of main()
	
	private static void sendReceive(DatagramPacket packet, String msg) throws IOException {        
		System.out.println("[send data] " + msg);
		System.out.println("Seq Num = " + seqNum);
		int sendTries = 0;
		int receiveTries = 0;
		long time = System.currentTimeMillis();
		// wait until we get the correct ack for this packet
		while(true) {
			if(sendTries > 0) {
				System.out.println("[resend data, attempt " + sendTries + "] " + msg);					
			}

			socket.send(packet);
			
			byte[] ackBuff = new byte[Header.HEADER_SIZE];
			DatagramPacket ackPacket = new DatagramPacket(ackBuff, ackBuff.length);
			socket.setSoTimeout((int)curTimeout);
			try {
				socket.receive(ackPacket);			
			}
        	// we timed out, so repeat loop (resend)
			catch(SocketTimeoutException e) {
				++sendTries;
				continue;
			}
			
			byte[] serializedAckHeader = new byte[Header.HEADER_SIZE];
			System.arraycopy(ackPacket.getData(), 0, serializedAckHeader, 0, Header.HEADER_SIZE);
			Header ackHeader = Header.deserialize(serializedAckHeader);			

			/*
			 * TODO: check the checksum of ackHeader
			 */
			
			receiveTries = 0;
			// if received an old ack (as result from duplicated, delayed, or reordered)
			while(ackHeader.getAckNum() < seqNum) {
				++receiveTries;
				System.out.println("RECEIVED OLD ACK: ackNum = " + ackHeader.getAckNum() + 
						". Current (last sent) seq num = " + seqNum + ". Highest received ack = " + 
						highestAckNum + ". Expecting/waiting for ack num = " + seqNum);
				System.out.println("[retrying receive, attempt " + receiveTries + "] " + msg);	
				
				ackBuff = new byte[Header.HEADER_SIZE];
				ackPacket = new DatagramPacket(ackBuff, ackBuff.length);
				socket.setSoTimeout((int)curTimeout);
				try {
					socket.receive(ackPacket);			
				}
	        	// we timed out, so repeat send
				catch(SocketTimeoutException e) {
					break;
				}

				serializedAckHeader = new byte[Header.HEADER_SIZE];
				System.arraycopy(ackPacket.getData(), 0, serializedAckHeader, 0, Header.HEADER_SIZE);
				ackHeader = Header.deserialize(serializedAckHeader);
			}
			
			// if received the correct ack for this send/receive of the packet
			if(ackHeader.getAckNum() == seqNum) {
				System.out.println("[ACK received] for packet seq # = " + seqNum + 
						". Ack num = " + ackHeader.getAckNum());

				if(!Utilities.correctChecksum(ackPacket)) {
					System.out.println("[Corrupt Packet] for seq # = " + seqNum + 
							"Ack num = " + ackHeader.getAckNum() + ". Resend this packet");
					++sendTries;
					continue;
				}
				
				highestAckNum = ackHeader.getAckNum();
				break;
			}
			
			++sendTries;
		}
		time = System.currentTimeMillis() - time;
		// update average round trip time if send/receive succeeded with no problems
		if(sendTries == 0) {
			roundTripTimes.add(time);
			System.out.println("Current avg RTT = " + roundTripTimes.getAverage() + "ms");
			// update current timeout to be average round trip time
			/*curTimeout = roundTripTimes.getAverage();
			System.out.println("Current Timeout Time (avg RTT) = " + curTimeout + "ms");*/
		}
		
	}
	
	private static byte[] createPacketBuffer(Header header, byte[] data) throws IOException {
	    // serialize packet header
		byte[] serializedHeader = header.serialize();
					
		byte[] serializedPacket = new byte[serializedHeader.length + data.length];
		System.arraycopy(serializedHeader, 0, serializedPacket, 0, serializedHeader.length);
		System.arraycopy(data, 0, serializedPacket, serializedHeader.length, data.length);
		
		return serializedPacket;
	}
	
	private static DatagramPacket createDatagramPacket(byte[] packetBuffer, InetAddress address, int port) {
		// construct the packet
		DatagramPacket packet = new DatagramPacket(packetBuffer, 
												   packetBuffer.length,
												   address,
												   port);
		
		return packet;
	}
}